//
// Crack
//
// Copyright (C) jextra.net.
//
//  This file is part of the Crack build system.
//
//  The Crack build system is free software; you can redistribute it 
//  and/or modify it under the terms of the GNU Lesser General Public
//  License as published by the Free Software Foundation; either
//  version 2.1 of the License, or (at your option) any later version.
//
//  The Crack build system is distributed in the hope that it will be 
//  useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
//  Lesser General Public License for more details.
//
//  You should have received a copy of the GNU Lesser General Public
//  License along with the Crack build system; if not, write to the Free
//  Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA
//  02111-1307 USA.
//

package net.jextra.crack.core;

import java.io.*;
import java.lang.reflect.*;
import java.util.*;
import javax.tools.*;
import net.jextra.crack.*;
import org.xml.sax.*;

/**
 *  <p>
 *  This is the singleton with a "main" method in it that responds to the "crack" script.
 *  </p>
 *  <p>It performs a number of basic "bootstraping" functions:</p>
 *  <ol>
 *      <li>Check that CRACK_HOME property is set and is an existing directory.
 *      <li>Search for Build.crack file in current directory (or overridden via -f option).
 *      <li>Compile the Build file.
 *      <li>Run the given commands (or just run help if none are specified).
 *  </ol>
 */
public class Crack extends CrackApp
{
    // ============================================================
    // Fields
    // ============================================================

    public static final int BUFFER_SIZE = 10000;
    public static final int MAX_DIR_SEARCH_DEPTH = 10;
    public static final String DEFAULT_FILE_NAME_1 = "Build.crack";
    public static final String DEFAULT_FILE_NAME_2 = "Build.java";
    public static final String LOCATOR_FILE = "crack.xml";

    public static final int STATUS_SUCCESS = 0;
    public static final int STATUS_FAILED = 1;

    private String buildFileName;
    private File buildFile;
    private boolean verbose;

    // ============================================================
    // Constructors
    // ============================================================

    public Crack()
        throws Exception
    {
    }

    // ============================================================
    // Methods
    // ============================================================

    // ----------
    // public
    // ----------

    public static void main( String[] args )
    {
        Crack engine = null;
        boolean failed = true;
        long now = System.currentTimeMillis();
        try
        {
            engine = new Crack();
            engine.execute( args );
            failed = false;
        }
        catch ( CrackError ex )
        {
            if ( engine.verbose )
            {
                ex.printStackTrace();
            }
            else
            {
                ex.print();
            }
        }
        catch ( Throwable ex )
        {
            ex.printStackTrace();
        }

        long sec = System.currentTimeMillis() - now;
        sec /= 1000;
        System.out.printf( "\n(%sTotal time: %d seconds)\n", failed ? "FAILED " : "", sec );

        int status = failed ? STATUS_FAILED : STATUS_SUCCESS;
        System.exit( status );
    }

    public static void printHelpHeader()
    {
        System.out.println( "Usage: crack [options] command <arguments>" );
        System.out.println();
        System.out.println( "Options:" );
        System.out.println();
        System.out.println( "  -f buildfile   Specify build file name (default is Build.crack)." );
        System.out.println( "  -version       Output Crack version." );
        System.out.println();
    }

    // ----------
    // protected
    // ----------

    //    protected static void registerShutdownListener( ShutdownListener process )
    //    {
    //    }

    // ----------
    // private
    // ----------

    private void execute( String[] args )
        throws Exception
    {
        verifyCrackHome();

        List<ParsedCommand> commands = parseCommands( args );

        //
        // Search for build file.
        //
        buildFile = null;
        if ( buildFileName == null )
        {
            buildFile = findBuildFile( new File( "." ) );
        }
        else
        {
            buildFile = new File( buildFileName );
        }

        if ( buildFile == null || !buildFile.exists() )
        {
            printHelpHeader();
            throw new CrackError( "No build file '%s' found.", buildFileName == null ? DEFAULT_FILE_NAME_1
                : buildFileName );
        }

        CrackContext.getInstance().setBuildFile( buildFile );

        //
        // Create working directory.
        //
        File workDir = new File( CrackContext.getInstance().getWorkDir(), getWorkDirName( buildFile ) );
        deleteDir( workDir );
        workDir.mkdirs();
        File classesDir = new File( workDir, "classes" );
        classesDir.mkdirs();
        CrackContext.getInstance().getCrackClassLoader().addPath( classesDir );

        //
        // Compile the build file.
        //
        int status = compileBuildFile( buildFile, workDir );
        if ( status != 0 )
        {
            throw new CrackError( "Compiling of Crack build file [%s] failed.", buildFile.getName() );
        }

        //
        // Instantiate the Build.class.
        //
        String buildPackage = getBuildPackage( classesDir, "" );
        String name = buildFile.getName();
        int period = name.lastIndexOf( '.' );
        if ( period >= 0 )
        {
            name = name.substring( 0, period );
        }
        String buildClassName = buildPackage + name;

        Class<?> contextClass = CrackContext.getInstance().getCrackClassLoader()
            .loadClass( CrackContext.class.getCanonicalName() );
        Object context = contextClass.getMethod( "getInstance", new Class<?>[]
        {} ).invoke( null, new Object[]
        {} );
        contextClass.getMethod( "setBuildFile", File.class ).invoke( context, buildFile );

        Class<?> buildClass = CrackContext.getInstance().getCrackClassLoader().loadClass( buildClassName );
        Object build = buildClass.newInstance();

        // Add build' own commands after constructor has run.
        buildClass.getMethod( "addCommands", Object.class ).invoke( build, build );
        //        build.addCommands( build );

        // Default command is "help"
        if ( commands.isEmpty() )
        {
            commands.add( new ParsedCommand( CMD_HELP ) );
        }

        //
        // Delete working directory.
        //
        deleteDir( workDir );

        //
        // Invoke commands.
        //
        for ( ParsedCommand command : commands )
        {
            buildClass.getMethod( "resetCommandLevel" ).invoke( build );

            for ( Method method : buildClass.getMethods() )
            {
                if ( !method.getName().equals( "invokeCommand" ) )
                {
                    continue;
                }

                Class<?>[] types = method.getParameterTypes();
                if ( types.length != 2 )
                {
                    continue;
                }

                if ( types[0] != String.class || types[1] != String[].class )
                {
                    continue;
                }

                method.invoke( build, command.getName(), command.getArgsArray() );
                break;
            }
            //            build.resetCommandLevel();
            //            build.invokeCommand( command.getName(), command.getArgsArray() );
        }
    }

    private File findBuildFile( File dir )
        throws SAXException, IOException
    {
        File buildFile = new File( dir, DEFAULT_FILE_NAME_1 );
        if ( buildFile.exists() )
        {
            return buildFile;
        }

        buildFile = new File( dir, DEFAULT_FILE_NAME_2 );
        if ( buildFile.exists() )
        {
            return buildFile;
        }

        File locatorFile = new File( dir, LOCATOR_FILE );
        if ( locatorFile.exists() )
        {
            File locatorDir = readLocatorFile( locatorFile );

            if ( locatorDir != null && locatorDir.exists() )
            {
                return findBuildFile( locatorDir );
            }
        }

        Collection<File> files = findBuildFiles( dir, 0 );
        if ( !files.isEmpty() )
        {
            if ( files.size() > 1 )
            {
                StringBuilder builder = new StringBuilder();
                for ( File file : files )
                {
                    if ( builder.length() > 0 )
                    {
                        builder.append( ",\n" );
                    }
                    builder.append( file.getCanonicalPath() );
                }
                throw new CrackException( "Found more than one " + DEFAULT_FILE_NAME_1 + " file\n(" + builder + ")." );
            }

            return files.iterator().next();
        }

        return null;
    }

    private Collection<File> findBuildFiles( File dir, int depth )
    {
        ArrayList<File> buildFiles = new ArrayList<File>();

        for ( File file : dir.listFiles( new BuildFilenameFilter() ) )
        {
            if ( file.isDirectory() && depth < MAX_DIR_SEARCH_DEPTH )
            {
                buildFiles.addAll( findBuildFiles( file, depth + 1 ) );
            }
            else
            {
                buildFiles.add( file );
            }
        }

        return buildFiles;
    }

    private File readLocatorFile( File file )
        throws SAXException, IOException
    {
        CrackLocatorReader reader = new CrackLocatorReader();
        reader.parse( file );

        return reader.getBuildFileDir();
    }

    private List<ParsedCommand> parseCommands( String[] args )
        throws Exception
    {
        ArrayList<ParsedCommand> commands = new ArrayList<ParsedCommand>();
        ParsedCommand command = null;

        for ( int i = 0; i < args.length; i++ )
        {
            if ( args[i].equals( "-version" ) )
            {
                System.out.println();
                printCrackVersion();
                return null;
            }
            else if ( args[i].equals( "-f" ) )
            {
                if ( i + 1 >= args.length )
                {
                    throw new CrackError( "-f argument must be followed by a file name." );
                }

                buildFileName = args[++i];
            }
            else if ( args[i].contains( "," ) )
            {
                String[] cmds = args[i].split( "," );
                for ( String cmd : cmds )
                {
                    command = new ParsedCommand( cmd );
                    commands.add( command );
                }
            }
            else
            {
                if ( command == null )
                {
                    command = new ParsedCommand( args[i] );
                    commands.add( command );
                }
                else
                {
                    command.addArg( args[i] );
                }
            }
        }

        return commands;
    }

    /**
     * Create an arbitrary unique working directory name from the given build file.
     */
    private String getWorkDirName( File buildFile )
        throws IOException
    {
        char[] badChars =
        { '/', '\\', ':', '.', ';' };

        String name = buildFile.getCanonicalPath().toLowerCase();
        for ( char c : badChars )
        {
            name = name.replace( c, '_' );
        }

        return name;
    }

    private void deleteDir( File dir )
    {
        if ( !dir.exists() )
        {
            return;
        }

        for ( File file : dir.listFiles() )
        {
            if ( file.isDirectory() )
            {
                deleteDir( file );
            }
            else
            {
                file.delete();
            }
        }

        dir.delete();
    }

    private int compileBuildFile( File buildFile, File workDir )
        throws Exception
    {
        System.out.printf( "(Build file: %s)\n", buildFile.getCanonicalPath() );

        //
        //
        //
        File classesDir = new File( workDir, "classes" );
        classesDir.mkdirs();

        //
        // Create exact copy of tree structure where Build.crack file is. This is required because
        // Build.crack -> Build.java can not be done in the source directory because the source
        // directory may be read-only.
        //
        File codeDir = new File( workDir, "code" );
        Stack<String> dirNames = new Stack<String>();
        for ( File parent = buildFile.getCanonicalFile().getParentFile(); parent != null; parent = parent
            .getParentFile() )
        {
            dirNames.push( parent.getName() );
        }

        while ( !dirNames.isEmpty() )
        {
            codeDir = new File( codeDir, dirNames.pop() );
        }
        codeDir.mkdirs();

        //
        // Copy Build.crack to file with required .java extension.
        //
        FileInputStream in = new FileInputStream( buildFile );
        File tempBuildFile = new File( codeDir, "Build.java" );
        FileOutputStream out = new FileOutputStream( tempBuildFile );
        byte[] buf = new byte[BUFFER_SIZE];
        int i = 0;
        while ( ( i = in.read( buf ) ) != -1 )
        {
            out.write( buf, 0, i );
        }
        in.close();
        out.close();

        StringBuilder classpath = new StringBuilder();

        //
        // Add all jars in the "libs" directory of crack.
        //
        Collection<File> jarFiles = CrackContext.getInstance().getCrackClasspath();
        for ( File jarFile : jarFiles )
        {
            classpath.append( jarFile.getCanonicalFile() );
            classpath.append( File.pathSeparator );
        }

        //
        // Append cracks classpath to the end.
        //
        classpath.append( System.getProperty( "java.class.path" ) );

        ArrayList<String> opts = new ArrayList<String>();
        opts.add( "-cp" );
        opts.add( classpath.toString() );
        opts.add( "-d" );
        opts.add( classesDir.getCanonicalPath() );

        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        if ( compiler == null )
        {
            throw new CrackError( "Unable to find Java compiler. Make sure JAVA_HOME points to a JDK and not a JRE." );
        }
        StandardJavaFileManager fileManager = compiler.getStandardFileManager( null, null, null );
        Iterable<? extends JavaFileObject> sources = fileManager.getJavaFileObjects( tempBuildFile );
        JavaCompiler.CompilationTask task = compiler.getTask( null, null, null, opts, null, sources );
        boolean success = task.call();
        return success ? 0 : 1;
    }

    /**
     * Search sub-dirs for the single .class file that was compiled.
     */
    private String getBuildPackage( File dir, String parentPackage )
    {
        // Pick off first item in the directory. If a dir, then dive into that dir,
        // if a file, then we are done.
        File file = dir.listFiles()[0];

        if ( file.isFile() )
        {
            return parentPackage;
        }

        return getBuildPackage( file, String.format( "%s%s.", parentPackage, file.getName() ) );
    }

    // ============================================================
    // Inner Classes
    // ============================================================

    private class ParsedCommand
    {
        private String name;
        private ArrayList<String> args;

        public ParsedCommand( String name )
        {
            this.name = name;
            args = new ArrayList<String>();
        }

        public String getName()
        {
            return name;
        }

        public void addArg( String arg )
        {
            args.add( arg );
        }

        public String[] getArgsArray()
        {
            String[] array = new String[args.size()];
            for ( int i = 0; i < args.size(); i++ )
            {
                array[i] = args.get( i );
            }

            return array;
        }
    }

    private class BuildFilenameFilter implements FilenameFilter
    {
        @Override
        public boolean accept( File dir, String name )
        {
            try
            {
                File file = new File( dir, name );

                if ( file.isDirectory() && !file.getName().startsWith( "." ) )
                {
                    return true;
                }

                return file.getName().equalsIgnoreCase( DEFAULT_FILE_NAME_1 );
            }
            catch ( Exception ex )
            {
                throw new RuntimeException( ex );
            }
        }
    }
}
